package com.hk.core.elasticsearch.query;

import com.hk.commons.util.ArrayUtils;
import com.hk.commons.util.CollectionUtils;
import com.hk.commons.util.ConverterUtils;
import com.hk.commons.util.StringUtils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.elasticsearch.core.query.Criteria;

import java.util.EnumMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;

/**
 * @author Kevin
 * @date 2019/3/11 9:27
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SimpleCondition implements Condition {

    private String field;

    private Criteria.OperationKey operator;

    private Object value;

    private static Map<Criteria.OperationKey, BiFunction<String, Object, Criteria>> operationKeyFunc;

    static {
        operationKeyFunc = new EnumMap<>(Criteria.OperationKey.class);
        operationKeyFunc.put(Criteria.OperationKey.EQUALS, (field, value) -> Criteria.where(field).is(value));
        operationKeyFunc.put(Criteria.OperationKey.CONTAINS, (field, value) -> {
            var valStr = ConverterUtils.defaultConvert(value, String.class);
            return StringUtils.isEmpty(valStr) ? null : Criteria.where(field).contains(valStr);
        });
        operationKeyFunc.put(Criteria.OperationKey.EXPRESSION, (field, value) ->
                Criteria.where(field).expression(ConverterUtils.defaultConvert(value, String.class)));
        operationKeyFunc.put(Criteria.OperationKey.FUZZY, (field, value) -> Criteria.where(field).fuzzy(ConverterUtils.defaultConvert(value, String.class)));
        operationKeyFunc.put(Criteria.OperationKey.STARTS_WITH, (field, value) -> {
            var valStr = ConverterUtils.defaultConvert(value, String.class);
            return StringUtils.isEmpty(valStr) ? null : Criteria.where(field).startsWith(valStr);
        });
        operationKeyFunc.put(Criteria.OperationKey.ENDS_WITH, (field, value) -> {
            var valStr = ConverterUtils.defaultConvert(value, String.class);
            return StringUtils.isEmpty(valStr) ? null : Criteria.where(field).endsWith(valStr);
        });
        operationKeyFunc.put(Criteria.OperationKey.WITHIN, (field, value) -> {
            if (value instanceof Object[] valueArr) {
                if (ArrayUtils.length(valueArr) == 2) {
                    return within(field, valueArr[0], valueArr[1]);
                }
            } else if (value instanceof Iterable<?> iterable && CollectionUtils.size(iterable) == 2) {
                Iterator<?> iterator = iterable.iterator();
                return within(field, iterator.next(), iterator.next());
            }
            return null;
        });
        operationKeyFunc.put(Criteria.OperationKey.BBOX, (field, value) -> {
            if (value instanceof Object[] valueArr) {
                if (ArrayUtils.length(valueArr) == 2) {
                    return bbox(field, valueArr[0], valueArr[1]);
                }
            } else if (value instanceof Iterable<?> iterable && CollectionUtils.size(iterable) == 2) {
                Iterator<?> iterator = iterable.iterator();
                return bbox(field, iterator.next(), iterator.next());
            }
            return null;
        });
        operationKeyFunc.put(Criteria.OperationKey.GREATER, (field, value) -> Objects.nonNull(value) ? Criteria.where(field).greaterThan(value) : null);
        operationKeyFunc.put(Criteria.OperationKey.GREATER_EQUAL, (field, value) -> Objects.nonNull(value) ? Criteria.where(field).greaterThanEqual(value) : null);
        operationKeyFunc.put(Criteria.OperationKey.LESS, (field, value) -> Objects.nonNull(value) ? Criteria.where(field).lessThan(value) : null);
        operationKeyFunc.put(Criteria.OperationKey.LESS_EQUAL, (field, value) -> Objects.nonNull(value) ? Criteria.where(field).lessThanEqual(value) : null);
        operationKeyFunc.put(Criteria.OperationKey.IN, (field, value) -> {
            if (value instanceof Iterable) {
                return Criteria.where(field).in((Iterable<?>) value);
            } else {
                return Criteria.where(field).in(value);
            }
        });
        operationKeyFunc.put(Criteria.OperationKey.NOT_IN, (field, value) -> {
            if (value instanceof Iterable) {
                return Criteria.where(field).not().in((Iterable<?>) value);
            } else {
                return Criteria.where(field).not().in(value);
            }
        });
        operationKeyFunc.put(Criteria.OperationKey.BETWEEN, (field, value) -> {
            if (value instanceof Object[] valueArr) {
                if (ArrayUtils.length(valueArr) == 2) {
                    return between(field, valueArr[0], valueArr[1]);
                }
            } else if (value instanceof Iterable<?> iterable && CollectionUtils.size(iterable) == 2) {
                Iterator<?> iterator = iterable.iterator();
                return between(field, iterator.next(), iterator.next());
            }
            return null;
        });

    }

    public SimpleCondition(String field, Object value) {
        this(field, Criteria.OperationKey.EQUALS, value);
    }

    @Override
    public Criteria toCriteria() {
        if (StringUtils.isEmpty(field)) {
            return null;
        }
        if (Objects.isNull(operator)) {
            operator = Criteria.OperationKey.EQUALS;
        }
        BiFunction<String, Object, Criteria> function = operationKeyFunc.get(operator);
        return Objects.isNull(function) ? null : function.apply(field, value);
    }

    private static Criteria between(String field, Object geoLocation, Object distance) {
        if (Objects.nonNull(geoLocation) && Objects.nonNull(distance)) {
            return Criteria.where(field).between(geoLocation, distance);
        }
        return null;
    }

    private static Criteria bbox(String field, Object topLeftGeo, Object bottomRightGeo) {
        var topLeft = ConverterUtils.defaultConvert(topLeftGeo, String.class);
        var bottomRight = ConverterUtils.defaultConvert(bottomRightGeo, String.class);
        if (StringUtils.isNotEmpty(topLeft) && StringUtils.isNotEmpty(bottomRight)) {
            return Criteria.where(field).boundedBy(topLeft, bottomRight);
        }
        return null;
    }

    private static Criteria within(String field, Object geoLocation, Object distance) {
        var geo = ConverterUtils.defaultConvert(geoLocation, String.class);
        var dis = ConverterUtils.defaultConvert(distance, String.class);
        if (StringUtils.isNotEmpty(geo) && StringUtils.isNotEmpty(dis)) {
            return Criteria.where(field).within(geo, dis);
        }
        return null;
    }
}
